/*
 * Copyright 2012 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.service.chataroundpush.server;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;

import com.google.appengine.api.datastore.DatastoreService;
import com.google.appengine.api.datastore.DatastoreServiceFactory;
import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.EntityNotFoundException;
import com.google.appengine.api.datastore.FetchOptions;
import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.KeyFactory;
import com.google.appengine.api.datastore.PreparedQuery;
import com.google.appengine.api.datastore.Query;
import com.google.appengine.api.datastore.Query.FilterOperator;
import com.google.appengine.api.datastore.Transaction;

/**
 * Simple implementation of a data store using standard Java collections.
 * <p>
 * This class is neither persistent (it will lost the data when the app is
 * restarted) nor thread safe.
 */
public final class PushDatastore {

	public static final int MULTICAST_SIZE = 1000;
	private static final String DEVICE_TYPE = "Device";
	private static final String DEVICE_REG_ID_PROPERTY = "regId";
	private static final String DEVICE_APP_ID_PROPERTY = "appId";
	private static final String DEVICE_REGISTRAATION_DATE_PROPERTY = "creationDate";

	private static final String MULTICAST_TYPE = "Multicast";
	private static final String MULTICAST_REG_IDS_PROPERTY = "regIds";

	private static final FetchOptions DEFAULT_FETCH_OPTIONS = FetchOptions.Builder
			.withPrefetchSize(MULTICAST_SIZE).chunkSize(MULTICAST_SIZE);

	private static final Logger logger = Logger.getLogger(PushDatastore.class
			.getName());
	private static final DatastoreService datastore = DatastoreServiceFactory
			.getDatastoreService();

	private PushDatastore() {
		throw new UnsupportedOperationException();
	}

	/**
	 * Registers a device.
	 * 
	 * @param regId
	 *            device's registration id.
	 */
	public static void register(String regId, String appId,Date registerDate) {

		logger.info("Registering " + regId + " to appId " + appId);

		Transaction txn = datastore.beginTransaction();
		try {
			if(regId!=null)
			regId=regId.trim();
			
			Entity entity = findDeviceByRegId(regId, appId);
			if (entity != null) {
				logger.fine(regId + " is already registered; ignoring.");
				return;
			}
			entity = new Entity(DEVICE_TYPE);
			entity.setProperty(DEVICE_REG_ID_PROPERTY, regId);
			entity.setProperty(DEVICE_APP_ID_PROPERTY, appId);
			entity.setProperty(DEVICE_REGISTRAATION_DATE_PROPERTY, registerDate);
			
			datastore.put(entity);
			txn.commit();
		} finally {
			if (txn.isActive()) {
				txn.rollback();
			}
		}
	}

	/**
	 * Unregisters a device.
	 * 
	 * @param regId
	 *            device's registration id.
	 */
	public static void unregister(String regId, String appId) {
		logger.info("Unregistering " + regId);
		Transaction txn = datastore.beginTransaction();
		try {
			if(regId!=null)
			regId=regId.trim();
			
			Entity entity = findDeviceByRegId(regId, appId);
			if (entity == null) {
				logger.warning("Device " + regId + " already unregistered");
			} else {
				Key key = entity.getKey();
				logger.info("Deleting Device " + regId + " unregistered");
				datastore.delete(key);
			}
			txn.commit();
		} finally {
			if (txn.isActive()) {
				txn.rollback();
			}
		}
	}

	/**
	 * Updates the registration id of a device.
	 */
	public static void updateRegistration(String oldId, String newId,
			String appId) {
		logger.info("Updating " + oldId + " to " + newId);
		Transaction txn = datastore.beginTransaction();
		try {
			Entity entity = findDeviceByRegId(oldId, appId);
			if (entity == null) {
				logger.warning("No device for registration id " + oldId);
				return;
			}
			entity.setProperty(DEVICE_REG_ID_PROPERTY, newId);
			datastore.put(entity);
			txn.commit();
		} finally {
			if (txn.isActive()) {
				txn.rollback();
			}
		}
	}
	/**
	 * Gets all registered devices.
	 */
	public static Map<String,String> getDevicesToMessage(String appId) {
		//List<String> devices;
		Map<String,String> deviceMap = new HashMap<String,String>(0);
		Transaction txn = datastore.beginTransaction();
		try {
			Query query = new Query(DEVICE_TYPE)
			.addFilter(DEVICE_APP_ID_PROPERTY, FilterOperator.EQUAL,
							appId);
			// add extra params to register the device
			Iterable<Entity> entities = datastore.prepare(query).asIterable(
					DEFAULT_FETCH_OPTIONS);
			//devices = new ArrayList<String>();
			
			int k=0;
			for (Entity entity : entities) {
				String device = (String) entity
						.getProperty(DEVICE_REG_ID_PROPERTY);
				//devices.add(device);
				deviceMap.put(device, device);
				k++;
			}
			txn.commit();
		} finally {
			if (txn.isActive()) {
				txn.rollback();
			}
		}
		return deviceMap;
	}
	
	/**
	 * Gets all registered devices.
	 */
	public static List<String> getDevices(String regId, String appId) {
		List<String> devices;
		Transaction txn = datastore.beginTransaction();
		try {
			Query query = new Query(DEVICE_TYPE).addFilter(
					DEVICE_REG_ID_PROPERTY, FilterOperator.EQUAL, regId)
					.addFilter(DEVICE_APP_ID_PROPERTY, FilterOperator.EQUAL,
							appId);
			// add extra params to register the device

			Iterable<Entity> entities = datastore.prepare(query).asIterable(
					DEFAULT_FETCH_OPTIONS);
			devices = new ArrayList<String>();

			for (Entity entity : entities) {
				String device = (String) entity
						.getProperty(DEVICE_REG_ID_PROPERTY);
				devices.add(device);
			}
			txn.commit();
		} finally {
			if (txn.isActive()) {
				txn.rollback();
			}
		}
		return devices;
	}

	/**
	 * Gets the number of total devices.
	 */
	public static int getTotalDevices() {
		Transaction txn = datastore.beginTransaction();
		try {
			Query query = new Query(DEVICE_TYPE).setKeysOnly();
			List<Entity> allKeys = datastore.prepare(query).asList(
					DEFAULT_FETCH_OPTIONS);
			int total = allKeys.size();
			logger.fine("Total number of devices: " + total);
			txn.commit();
			return total;
		} finally {
			if (txn.isActive()) {
				txn.rollback();
			}
		}
	}

	private static Entity findDeviceByRegId(String regId, String appId) {
		Query query = new Query(DEVICE_TYPE).addFilter(DEVICE_REG_ID_PROPERTY,
				FilterOperator.EQUAL, regId).addFilter(DEVICE_APP_ID_PROPERTY,
				FilterOperator.EQUAL, appId);
		// add extra params to register the device
		PreparedQuery preparedQuery = datastore.prepare(query);
		List<Entity> entities = preparedQuery.asList(DEFAULT_FETCH_OPTIONS);
		Entity entity = null;
		if (!entities.isEmpty()) {
			entity = entities.get(0);
		}
		int size = entities.size();
		if (size > 0) {
			logger.severe("Found " + size + " entities for regId " + regId
					+ ": " + entities);
		}
		return entity;
	}

	/**
	 * Creates a persistent record with the devices to be notified using a
	 * multicast message.
	 * 
	 * @param devices
	 *            registration ids of the devices.
	 * @return encoded key for the persistent record.
	 */
	public static String createMulticast(List<String> devices) {
		logger.info("Storing multicast for " + devices.size() + " devices");
		String encodedKey;
		Transaction txn = datastore.beginTransaction();
		try {
			Entity entity = new Entity(MULTICAST_TYPE);
			entity.setProperty(MULTICAST_REG_IDS_PROPERTY, devices);
			datastore.put(entity);
			Key key = entity.getKey();
			encodedKey = KeyFactory.keyToString(key);
			logger.fine("multicast key: " + encodedKey);
			txn.commit();
		} finally {
			if (txn.isActive()) {
				txn.rollback();
			}
		}
		return encodedKey;
	}

	/**
	 * Gets a persistent record with the devices to be notified using a
	 * multicast message.
	 * 
	 * @param encodedKey
	 *            encoded key for the persistent record.
	 */
	public static List<String> getMulticast(String encodedKey) {
		Key key = KeyFactory.stringToKey(encodedKey);
		Entity entity;
		Transaction txn = datastore.beginTransaction();
		try {
			entity = datastore.get(key);
			@SuppressWarnings("unchecked")
			List<String> devices = (List<String>) entity
					.getProperty(MULTICAST_REG_IDS_PROPERTY);
			txn.commit();
			return devices;
		} catch (EntityNotFoundException e) {
			logger.severe("No entity for key " + key);
			return Collections.emptyList();
		} finally {
			if (txn.isActive()) {
				txn.rollback();
			}
		}
	}

	/**
	 * Updates a persistent record with the devices to be notified using a
	 * multicast message.
	 * 
	 * @param encodedKey
	 *            encoded key for the persistent record.
	 * @param devices
	 *            new list of registration ids of the devices.
	 */
	public static void updateMulticast(String encodedKey, List<String> devices) {
		Key key = KeyFactory.stringToKey(encodedKey);
		Entity entity;
		Transaction txn = datastore.beginTransaction();
		try {
			try {
				entity = datastore.get(key);
			} catch (EntityNotFoundException e) {
				logger.severe("No entity for key " + key);
				return;
			}
			entity.setProperty(MULTICAST_REG_IDS_PROPERTY, devices);
			datastore.put(entity);
			txn.commit();
		} finally {
			if (txn.isActive()) {
				txn.rollback();
			}
		}
	}

	/**
	 * Deletes a persistent record with the devices to be notified using a
	 * multicast message.
	 * 
	 * @param encodedKey
	 *            encoded key for the persistent record.
	 */
	public static void deleteMulticast(String encodedKey) {
		Transaction txn = datastore.beginTransaction();
		try {
			Key key = KeyFactory.stringToKey(encodedKey);
			datastore.delete(key);
			txn.commit();
		} finally {
			if (txn.isActive()) {
				txn.rollback();
			}
		}
	}

}
